这是去年写的一篇总结了,一直没搬到我的博客上来,最近才想起。
本文相关代码 https://github.com/wave52/react-practise/blob/master/src/redux/dvaApp/index.js
问题:
- 使用最新的请求结果
- 取消一个耗时很长的请求(手动/超时自动)
- 轮询(循环定时发起)
- 排队请求(循环排队发起)
- 跨 model 时序问题
分析:
一.redux-saga
1.redux-saga是个中间件
那源码是否可以找到 redux-thunk 这样的中间件写法?(redux-thunk 源码简析之前文章做过了,14 行代码很简单)
可以,就在 /packages/core/src/internal/middleware.js(0.16.0 src/internal/middleware.js)
sagaMiddlewareFactory 这个函数等同于 createThunkMiddleware
其主要逻辑是
1 | return next => action => { |
使用时,对比thunk:
1 | // thunk |
1 | // saga |
上面的 thunk 中间件这样用以后,变化的是 reducer,本来 action 是一个 plainObject,现在的 action 可以是个方法了;而用了 saga 中间件以后,之前的 reducer 没有变化,action 还是一个 plainObject 表示 state 的变化,而异步的都放在 sagas(上面的 rootSaga)中处理了,至于 channel.put(action) 可以理解成 channel 是一个发布订阅模式,channel.put 就是发布,而 saga 中会订阅(take)
也就是说 saga 的程序逻辑会存在两个位置:
- Reducers 负责处理 action 的 state 转换
- Sagas 负责策划统筹合成和异步操作
源码中也能体现 const result = next(action),不像 thunk 还做了拦截处理,而 saga 的处理其实是在 run 函数,
run 函数源码对应 runSaga/packages/core/src/internal/runSaga.js
1 | const iterator = saga(...args) |
这里 saga 的 args 参数可能有各种情况,本例 saga 对应 rootSaga,...args 是空的。
确实,rootSaga 是一个 generator,他执行后返回一个 IterableIterator 类型的对象 iterator
1 | export default function* root() { |
那我们看看怎么处理这个 iterator 的
1 | return immediately(() => { |
作为参数传给了 proc 函数,外面包的这层函数先不用管,这里可以看成 (()=>{proc})() 一个立即执行函数,再看看 proc,这是重点了run(rootSaga)执行的调用栈(从proc开始,前面的都分析了)proc()
-> next()
-> result = iterator.next(arg) - (看过 generator 应该知道此时来到了第一个 yield all(), 打印一下{value:{type: ALL,....},done: false},判断 done 还不为 true,直接打印 console.log(all()); 也会看到这个结构)
-> digestEffect()
-> finalRunEffect() - (finalRunEffect()是在runSaga中封装了runeffect,runsaga可以接受一个effectMiddlewares参数进行封装TODO1)
-> runEffect()
-> 然后去 effectRunnerMap.js
-> runAllEffect()
-> forEach 调用 digestEffect() 同上第一个是 fork
-> runForkEffect()
-> proc
当前 iterator 就变成了 getAllProducts
更多 saga 原理与实现 http://frontend.dev-ag.xxx.com/blog/2018/08/28/redux-saga.html
我们主要关注 redux-saga 的使用与 dva 的原理和实现
只需强调几个地方帮助我们理解:
1.watch-and-fork 模式
2.阻塞(take,call…)与非阻塞(fork,put…)
3.saga-helper
2.dva 封装了啥
dva 调用 runstore.runSaga = sagaMiddleware.run;
injectModel 中 store.runSaga
参数应该是 rootSaga,那 dva 中是啥
是一个 getSaga 方法 dva/packages/dva-core/src/getSaga.js
返回是一个 funtion*() 就像 rootSaga 一样但里面没有用 all() 包装,而是一个 for 循环,毕竟 all() 中也是 forEach,然后
1 | const watcher = getWatcher(key, effects[key], model, onError, onEffect, opts); |
第一行每一个 saga 我们都用一个 sagaHelper 包装一下
第二行包装后的 saga 我们 fork 一下,就像前面的例子一样
这里就需要理解一下 sagaHelper 和 fork 的意义了
fork 是非阻塞的 call(https://redux-saga-in-chinese.js.org/docs/api/)
另外,createEffects return { ...sagaEffects, put, take }; 这里可以发现,dva 中是可以使用 redux-saga 中所有的 effects 的,不过要注意版本
3.saga helper(saga辅助函数)
saga helper 对应源码 io-helpers debounce, retry, takeEvery, takeLatest, takeLeading, throttle,saga 辅助函数是由effect创建器组合而来的高级API
takeEvery 是一个使用 take 和 fork 构建的高级 API。下面演示了这个辅助函数是如何由低级 Effect 实现的
1 | const takeEvery = (patternOrChannel, saga, ...args) => fork(function*() { |
dva 的 "redux-saga": "^0.16.0",包含的 saga helper 只有 takeEvery, takeLatest, throttle,对应 sagaHelpers 这个目录
dva 中的 saga helper 作为 effects type 存在
https://blog.csdn.net/wangweiren_get/article/details/89043113#_1
getWatcher 源码中默认用 takeEvery 包裹,所以一般没有写,问题 3 就可以用takeLatest来解决
我们具体来看看每一个 saga helper 的用途
1).先看看 redux-saga 本身的
takeEvery: 每次 dispatch action 都会触发
takeLatest: 每次 dispatch action 会销毁之前的,再触发
throttle: 节流,不多解释
2).dva依赖redux-saga@0.16.x,下面这些新的saga-helper你用dva时应该没有见过:
takeLeading: 每次dispatch触发一次,当执行完了才能在被触发,中间dispatch不会触发
debounce: 防抖,不多解释
retry: 失败自动重试
3).另外由于 saga helper 就是简单的 effects 创建器组合而成的高级API,所以 dva 在封装时也自己做了几个 saga helper(在dva里叫effect type)
watcher: 其实就就是没有用 saga-helper,初始化启动,这个名字来源是由于 saga 常见模式 watch-and-fork 模式,dva 只对其做了简单的 try catch 封装
poll(2.6beta版): 顾名思义是做轮询用的(解决了开头的问题),现在我们用的是 dva@2.4 版本还没有这个 effect,可以看下他怎么做的,总之是由简单的 effect 创建器组合而成,solvay 中就自己实现过
4.effect 创建器
https://redux-saga-in-chinese.js.org/docs/basics/DeclarativeEffects.html
作用:为了方便测试,把直接调用改为使用 effect 创建器包装调用
take、fork
5.channel
…
本文相关代码 https://github.com/wave52/react-practise/blob/master/src/redux/dvaApp/index.js